#!/usr/bin/env bash

USAGE="Usage: ${0##*/} <last> <commit> [...]"
START="$PWD"
LAST=
UPSTREAM=
COMMIT=
BRANCH=

die() {
	[ "$#" -eq 0 ] || echo "$*" >&2
	exit 1
}

err() {
	echo "$*" >&2
}

quit() {
	[ "$#" -eq 0 ] || echo "$*"
	exit 0
}

short() {
	git rev-parse --short "$1"
}

# returns the latest commit ID in $REPLY. Returns 0 on success, non-zero on
# failure with $REPLY empty.
get_last_commit() {
	REPLY=$(git rev-parse HEAD)
	test -n "$REPLY"
}

# returns the name of the current branch (1.8, 1.9, etc) in $REPLY. Returns 0
# on success, non-zero on failure with $REPLY empty.
get_branch() {
	local major subver ext
	REPLY=$(git describe --tags HEAD --abbrev=0 2>/dev/null)
	REPLY=${REPLY#v}
	subver=${REPLY#[0-9]*.[0-9]*[-.]*[0-9].}
	[ "${subver}" != "${REPLY}" ] || subver=""
	major=${REPLY%.$subver}
	ext=${major#*[0-9].*[0-9]}
	REPLY=${major%${ext}}
	test -n "$REPLY"
}

# returns the path to the next "up" remote in $REPLY, and zero on success
# or non-zero when the last one was reached.
up() {
	REPLY=$(git remote -v | awk '/^up\t.*\(fetch\)$/{print $2}')
	test -n "$REPLY"
}

# returns the path to the next "down" remote in $REPLY, and zero on success
# or non-zero when the last one was reached.
down() {
	REPLY=$(git remote -v | awk '/^down\t.*\(fetch\)$/{print $2}')
	test -n "$REPLY"
}

# verifies that the repository is clean of any pending changes
check_clean() {
	test -z "$(git status -s -uno)"
}

# verifies that HEAD is the master
check_master() {
	test "$(git rev-parse --verify -q HEAD 2>&1)" = "$(git rev-parse --verify -q master 2>&1)"
}

# tries to switch to the master branch, only if the current one is clean. Dies on failure.
switch_master() {
	check_clean || die "$BRANCH: local changes, stopping on commit $COMMIT (upstream $UPSTREAM)"
	git checkout master >/dev/null 2>&1 || die "$BRANCH: failed to checkout master, stopping on commit $COMMIT (upstream $UPSTREAM)"
}

# walk up to the first repo
walk_up() {
	cd "$START"
}

# updates the "up" remote repository. Returns non-zero on error.
update_up() {
	git remote update up >/dev/null 2>&1
}

# backports commit "$1" with a signed-off by tag. In case of failure, aborts
# the change and returns non-zero. Unneeded cherry-picks do return an error
# because we don't want to accidently backport the latest commit instead of
# this one, and we don't know this one's ID.
backport_commit() {
	local empty=1

	if ! git cherry-pick -sx "$1"; then
		[ -n "$(git diff)" -o -n "$(git diff HEAD)" ] || empty=0
		git cherry-pick --abort
		return 1
	fi
}

[ "$1" != "-h" -a "$1" != "--help" ] || quit "$USAGE"
[ -n "$1" -a -n "$2" ] || die "$USAGE"

LAST="$1"
shift

# go back to the root of the repo
cd $(git rev-parse --show-toplevel)
START="$PWD"

while [ -n "$1" ]; do
	UPSTREAM="$(short $1)"
        [ -n "$UPSTREAM" ] || die "branch $BRANCH: unknown commit ID $1, cannot backport."
	COMMIT="$UPSTREAM"
	BRANCH="-source-"
	while :; do
		if ! down; then
			err "branch $BRANCH: can't go further, is repository 'down' properly set ?"
			break
		fi

		cd "$REPLY" || die "Failed to 'cd' to '$REPLY' from '$PWD', is repository 'down' properly set ?"

		check_clean || die "Local changes in $PWD, stopping before backporting commit $COMMIT (upstream $UPSTREAM)"

		check_master || switch_master || die "Cannot switch to 'master' branch in $PWD, stopping before backporting commit $COMMIT (upstream $UPSTREAM)"
		get_branch || die "Failed to get branch name in $PWD, stopping before backporting commit $COMMIT (upstream $UPSTREAM)"
		BRANCH="$REPLY"

		update_up || die "$BRANCH: failed to update repository 'up', stopping before backporting commit $COMMIT (upstream $UPSTREAM)"

		backport_commit "$COMMIT" || die "$BRANCH: failed to backport commit $COMMIT (upstream $UPSTREAM). Leaving repository $PWD intact."

		if [ "$BRANCH" = "$LAST" ]; then
			# reached the stop point, don't apply further
			break
		fi

		get_last_commit || die "$BRANCH: cannot retrieve last commit ID, stopping after backporting commit $COMMIT (upstream $UPSTREAM)"
		COMMIT="$(short $REPLY)"
	done
	walk_up || die "Failed to go back to $PWD, stopping *after* backporting upstream $UPSTREAM"
	shift
done
